import sys
import json
import time
import logging
import requests
import argparse
import colorama
import re
from urllib.parse import urlparse, urlunparse
import posixpath
from colorama import Fore, Style
from jsonpath_ng import parse
from jsonpath_ng.exceptions import JSONPathError

# 初始化颜色库（自动处理Windows颜色支持）
colorama.init(autoreset=True)

# 配置日志格式
LOG_FORMAT = f"{Fore.CYAN}%(asctime)s {Style.RESET_ALL}| %(levelname)-8s | %(message)s"
FILE_FORMAT = "%(asctime)s | %(levelname)-8s | %(message)s"

# 自定义日志级别颜色
LOG_COLORS = {
    "INFO": Fore.GREEN,
    "DEBUG": Fore.BLUE,
    "WARNING": Fore.YELLOW,
    "ERROR": Fore.RED,
    "CRITICAL": Fore.RED + Style.BRIGHT,
}


# 去除ANSI转义码的Formatter
class NoColorFormatter(logging.Formatter):
    ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")

    def format(self, record):
        message = super().format(record)
        return self.ansi_escape.sub("", message)


# 配置Logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 控制台Handler
stream_handler = logging.StreamHandler(sys.stdout)
stream_formatter = logging.Formatter(LOG_FORMAT)
stream_handler.setFormatter(stream_formatter)

# 文件Handler
file_handler = logging.FileHandler("api_test.log", encoding="utf-8")
file_formatter = NoColorFormatter(FILE_FORMAT)
file_handler.setFormatter(file_formatter)

# 移除默认的handlers并添加自定义的
logger.handlers = []
logger.addHandler(stream_handler)
logger.addHandler(file_handler)


def build_full_url(base_url, relative_url):
    """构建完整的URL，处理相对路径和基础URL的拼接"""
    if not base_url or not relative_url:
        return relative_url or base_url
    if relative_url.lower().startswith(('http://', 'https://')):
        return relative_url
    parsed_base = urlparse(base_url)
    new_path = posixpath.join(parsed_base.path, relative_url)
    new_path = posixpath.normpath(new_path)
    if relative_url.endswith('/') and not new_path.endswith('/'):
        new_path += '/'
    parsed_result = parsed_base._replace(path=new_path)
    return urlunparse(parsed_result)

def load_json_file(file_path):
    """加载JSON文件并返回解析后的数据"""
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            logger.debug(f"[FILE] 正在加载文件: {file_path}...")
            data = json.load(f)
            logger.info(
                f"[OK] 成功加载 [{Fore.YELLOW}{file_path}{Style.RESET_ALL}]"
                f" (包含 {Fore.CYAN}{len(data)}{Style.RESET_ALL} 条数据)"
            )
            return data
    except FileNotFoundError:
        logger.error(f"[FAIL] 文件未找到: {Fore.RED}{file_path}{Style.RESET_ALL}")
        raise
    except json.JSONDecodeError as e:
        logger.error(f"JSON解析失败 [{file_path}]: {str(e)}")
        raise
    except Exception as e:
        logger.exception(f"加载文件时发生意外错误 [{file_path}]:")
        raise


def execute_test_step(api, step, step_num, dataset):
    """执行单个测试步骤并返回测试结果"""
    logger.info(f"\n{Fore.BLUE}▶▶ 步骤 {step_num} {'-'*50}{Style.RESET_ALL}")
    title = step.get("title", f"未命名测试步骤 {step_num}")
    logger.info(f"{Fore.CYAN}▷ 测试名称: {Fore.WHITE}{title}{Style.RESET_ALL}")
    
    
    # URL处理逻辑
    api_url = api.get("url") if api else None
    step_url = step.get("url")

    if not step_url and not api_url:
        error_msg = f"'{title}' 缺少URL参数"
        logger.error(error_msg)
        return error_msg, False

    # 构建完整URL
    if api_url and step_url:
        url = build_full_url(api_url, step_url)
    elif api_url:
        url = api_url
    else:
        url = step_url
    
    spath = step.get("spath")
    if not spath:
        error_msg = f"'{title}' 缺少必要参数 (spath)"
        logger.error(error_msg)
        return error_msg, False
    
    method = step.get("method", "GET").upper()
    params = step.get("params", {})
    request_data = step.get("data")
    
    # 获取预期值
    try:
        jsonpath_expr = parse(spath)
        matches = jsonpath_expr.find(dataset)
        expect_values = [match.value for match in matches]
        if not expect_values:
            error_msg = f"{title} | 错误：未找到路径 '{spath}' 对应的预期值"
            logger.error(error_msg)
            return error_msg, False
        expected_value = expect_values[0]
        logger.debug(f"预期值提取成功: {expected_value}")
    except JSONPathError as e:
        error_msg = f"{title} | JSONPath语法错误 [{spath}]: {str(e)}"
        logger.error(error_msg)
        return error_msg, False
    except Exception as e:
        error_msg = f"{title} | JSONPath处理异常: {str(e)}"
        logger.exception(error_msg)
        return error_msg, False

    # 发送HTTP请求
    try:
        logger.debug(
            f"{Fore.BLUE}>> 请求信息{Style.RESET_ALL}\n"
            f"{Fore.WHITE}{method} {url}\n"
            f"{Fore.CYAN}Params: {Fore.WHITE}{json.dumps(params, indent=2)}\n"
            f"{Fore.CYAN}Body: {Fore.WHITE}{json.dumps(request_data, indent=2)}{Style.RESET_ALL}"
        )
        
        # 移除可能存在的url参数
        api_kwargs = api.copy() if api else {}
        api_kwargs.pop('url', None)  
        
        start_time = time.perf_counter()
        response = requests.request(
            **api_kwargs,
            method=method,
            url=url,
            params=params,
            json=request_data,
        )
        elapsed = time.perf_counter() - start_time
        logger.debug(
            f"{Fore.GREEN}<< 响应 [{response.status_code}] {Style.RESET_ALL}"
            f"{Fore.CYAN}(耗时: {elapsed:.3f}s){Style.RESET_ALL}\n"
            f"{Fore.WHITE}{response.text[:300]}{'...' if len(response.text)>300 else ''}{Style.RESET_ALL}"
        )
        response.raise_for_status()
    except Exception as e:
        error_msg = f"{title} | 请求失败: {str(e)}"
        logger.error(f"{Fore.RED}## {error_msg}{Style.RESET_ALL}")
        if (
            isinstance(e, requests.RequestException)
            and hasattr(e, "response")
            and e.response
        ):
            logger.debug(f"错误响应内容: {e.response.text[:200]}")
        logger.debug(f"异常类型: {type(e).__name__}", exc_info=True)
        return error_msg, False

    # 处理响应数据
    try:
        content_type = response.headers.get("Content-Type", "")
        if "json" in content_type:
            response_data = response.json()
            logger.debug("成功解析JSON响应")
        else:
            response_data = response.text
            logger.debug("响应内容为非JSON格式")

        dpath = step.get("dpath")
        if dpath:
            try:
                jsonpath_expr = parse(dpath)
                matches = jsonpath_expr.find(response_data)
                actual_values = [match.value for match in matches]
                if not actual_values:
                    error_msg = f"{title} | 错误：未找到响应路径 '{dpath}'"
                    logger.error(error_msg)
                    return error_msg, False
                actual_value = actual_values[0]
                logger.debug(f"从响应路径提取的实际值: {actual_value}")
            except Exception as e:
                error_msg = f"{title} | 响应路径语法错误 [{dpath}]: {str(e)}"
                logger.error(error_msg)
                return error_msg, False
        else:
            actual_value = response.text.strip()
            logger.debug(f"完整响应内容: {actual_value}")
    except ValueError:
        actual_value = response.text.strip()
        logger.warning("响应内容不是有效的JSON，使用原始文本")
    except Exception as e:
        error_msg = f"{title} | 响应处理异常: {str(e)}"
        logger.exception(error_msg)
        return error_msg, False

    # 验证结果
    success = str(actual_value) == str(expected_value)
    result_msg = (
        f"{Fore.BLUE}>> 验证结果{Style.RESET_ALL}\n"
        f"{Fore.GREEN if success else Fore.RED}‣ {'通过' if success else '失败'} "
        f"{Fore.WHITE}(步骤 {step_num}){Style.RESET_ALL}\n"
        f"{Fore.CYAN}├─{Fore.WHITE} 预期: {Fore.YELLOW}{expected_value}{Style.RESET_ALL}\n"
        f"{Fore.CYAN}└─{Fore.WHITE} 实际: {Fore.MAGENTA}{actual_value}{Style.RESET_ALL}"
    )

    if success:
        logger.info(f"{Fore.GREEN}✔ 验证通过 {Style.RESET_ALL}{result_msg}")
    else:
        logger.error(f"{Fore.RED}✘ 验证失败 {Style.RESET_ALL}\n{result_msg}")

    return result_msg, success


def main(testcase_path):
    """主测试执行函数"""
    
    # 记录测试套件启动时间
    start_time = time.perf_counter()  
    
    logger.info(f"\n{Fore.BLUE}▶▶▶ API测试套件 ◀◀◀{Style.RESET_ALL}")
    logger.info(
        f"{Fore.CYAN}▪ 测试集: {Fore.WHITE}{testcase_path}{Style.RESET_ALL}\n"
    )
    logger.info("加载数据...")

    try:
        testcase = load_json_file(testcase_path)
    except Exception as e:
        logger.critical("加载数据失败，终止测试执行")
        return

    api = testcase.get("api", None)
    dataset = testcase.get("dataset", None)
    steps = testcase.get("steps", [])

    if not steps:
        logger.warning("测试用例中没有定义任何测试步骤")
        return

    total_steps = len(steps)
    logger.info(f"{Fore.CYAN}▪ 总步骤数: {Fore.WHITE}{total_steps}{Style.RESET_ALL}")
    success_count = 0

    for step_idx, step in enumerate(steps):
        step_number = step_idx + 1
        result, success = execute_test_step(api, step, step_number, dataset)
        logger.info(f"\n{Fore.BLUE}▶ 进度: ({step_number}/{total_steps}) ")

        if success:
            success_count += 1

        logger.debug(f"测试步骤 {step_number} 详细结果:\n{result}")

    # 计算整体运行耗时并格式化为“时：分：秒”
    total_time = time.perf_counter()  - start_time
    total_seconds = int(total_time)
    hours = total_seconds // 3600
    minutes = (total_seconds % 3600) // 60
    seconds = total_seconds % 60
    
    border_color = Fore.GREEN if success_count == len(steps) else Fore.RED

    summary = (
        f"\n{border_color}{'━'*40}{Style.RESET_ALL}\n"
        f"{Fore.BLUE}▶▶▶ 测试总结 ◀◀◀{Style.RESET_ALL}\n"
        f"{Fore.CYAN}▪ 总步骤数: {Fore.WHITE}{len(steps)}\n"
        f"{Fore.GREEN}▪ 通过: {success_count}\n"
        f"{Fore.RED}▪ 失败: {len(steps)-success_count}\n"
        f"{Fore.CYAN}▪ 通过率: {Fore.WHITE}{success_count/len(steps):.2%}{Style.RESET_ALL}\n"
        f"{Fore.CYAN}▪ 执行耗时: {Fore.WHITE}{hours}时:{minutes:02d}分:{seconds:02d}秒{Style.RESET_ALL}\n"
        f"{border_color}{'━'*40}{Style.RESET_ALL}"
    )
    logger.info(summary)

    if success_count == len(steps):
        logger.info(f"{Fore.GREEN}🎉 所有测试通过！ {Style.RESET_ALL}")
    else:
        logger.info(f"{Fore.RED}⚠ 存在失败用例，请检查日志 {Style.RESET_ALL}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="轻量级数据驱动API自动化测试工具",
        epilog="\n使用示例:\n"
        "  %(prog)s ./testcases_example.json\n"
        "  %(prog)s /path/to/testcases.json",
        formatter_class=argparse.RawTextHelpFormatter,
    )
    parser.add_argument(
        "testcases",
        type=str,
        help="测试用例文件路径（JSON格式）\n"
        "示例结构：\n"
        "  {\n"
        '    "api": {\n'
        '      "headers": {\n'
        '           "Content-Type": "application/json",\n'
        '           "Accept": "application/json",\n'
        '           "Authorization": "Bearer  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",\n'
        '           "timeout": 5\n'
        '     "url": "http://localhost:8000/"\n'
        "    },\n"
        '    "dataset":\n'
        "      {\n"
        '        "name": "数据集",\n'
        '        "desc": "数据集描述信息",\n'
        '        "path": "数据集相关资源路径信息",\n'
        '        "number": 500,\n'
        '        "proto": 100\n'
        "        ...\n"
        "      }\n"
        '    "steps": [\n'
        "      {\n"
        '        "title": "测试点1",\n'
        '        "spath": "$.name",\n'
        '        "dpath": "$.data.number,"\n'
        '        "method": "POST",\n'
        '        "url": "/get_number/",\n'
        '        "data": {\n'
        "        }\n"
        "      },\n"
        "      {\n"
        '        "title": "测试点2",\n'
        '        "spath": "$.proto",\n'
        '        "dpath": "$.data.proto",\n'
        '        "method": "POST",\n'
        '        "url": "/get_proto",\n'
        '        "data": {\n'
        "        }\n"
        "      },\n"
        "      ...\n"
        "    ]\n"
        "  }",
    )
    args = parser.parse_args()

    try:
        main(args.testcases)
    except Exception as e:
        logger.critical(f"未捕获的异常导致程序终止: {str(e)}", exc_info=True)
